package rolesystem.jade.agents.carry.carrier;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.concurrent.Semaphore;

import javax.swing.Timer;

import rolesystem.agentlogic.managers.ContactManager;
import rolesystem.agentlogic.managers.MovementManager;
import rolesystem.agentlogic.parameters.ConfigurationParameters;
import rolesystem.agentlogic.utilityclasses.CarrierData;
import rolesystem.core.RoleException;
import rolesystem.core.RoleRegistration;
import rolesystem.core.RoleSystem;
import rolesystem.jade.agents.SubjectLayer;
import rolesystem.middle.managers.BooleanManager;
import rolesystem.middle.managers.CommunicationManager;
import rolesystem.middle.managers.CounterManager;
import rolesystem.middle.managers.DataOutputManager;
import rolesystem.roles.carry.*;
import rolesystem.middle.managers.EnvironmentManager;
import rolesystem.middle.utilityclasses.AgentMap;
import rolesystem.middle.utilityclasses.EventData;
import rolesystem.middle.utilityclasses.MessagePayload;
import rolesystem.middle.utilityclasses.PositionSq;
import rolesystem.middle.utilityclasses.SearchResult;
import rolesystem.middle.utilityclasses.Square;


/**
 * This is a subject layer that can be used by agents in a role system.
 */
public class CarrierSL implements SubjectLayer
{			
	/**
	 * Variable to reset the agent only on time if no further changes are made.
	 */
	private boolean RESET_LOCAL=true;
	
	/**
	 * The weight that the agent is able to carry
	 */
	private int carry_weight;
	
	/**
	 * The identifier of the agent
	 */
	private int id;
	
	/**
	 * Reference to the Rolesystem
	 */
	private RoleSystem roleSystem=null;
	
	/**
	 * Timer to end the simulation if nothing happens for n time
	 */
	private static Timer end_timer;
	
	/**
	 * Timer to save the data periodically
	 */
	private static Timer data_timer;
	
	/**
	 * Manage the contact (with other agents) phase 
	 */
	private ContactManager contactManager;
	
	/**
	 * Manage the agent movements in the environment 
	 */
	private MovementManager movementManager;
	
	private CommunicationManager communicationManager;
	
	private static Semaphore semaphore;
	
	/**
	 * Static constructor.
	 */
	static
	{
		for(int i=0;i<ConfigurationParameters.counterNames.length;i++)
			CounterManager.add(ConfigurationParameters.counterNames[i]);
		
		for(int i=0;i<ConfigurationParameters.boolNames.length;i++)
			BooleanManager.add(ConfigurationParameters.boolNames[i],ConfigurationParameters.boolInitValues[i]);
		
		semaphore = new Semaphore(1);
		
		ActionListener taskPerformer = new ActionListener() 
		{
			public void actionPerformed(ActionEvent evt) 
			{
				if(CounterManager.isZero(ConfigurationParameters.MANAGER_COUNTER))
					BooleanManager.set(ConfigurationParameters.TIMER_END_BOOL, true);
				else
					end_timer.restart();
			}
		};
		
		end_timer= new Timer(ConfigurationParameters.END_TIMER_DELAY, taskPerformer);
		
		ActionListener dataTaskPerformer = new ActionListener() 
		{
			public void actionPerformed(ActionEvent evt) 
			{
				DataOutputManager.periodicAggregateDataToFile();
			}
		};
		
		data_timer=new Timer(ConfigurationParameters.DATA_SAVE_INTERVAL, dataTaskPerformer);
		data_timer.setRepeats(true);
		data_timer.stop();
	}
	
	/**
	 * Agent constructor 
	 * 
	 * @param  x_start  the position in witch the agent starts - X
	 * @param  y_start  the position in witch the agent starts - Y
	 * @param carry_weight the weight that the agent is able to carry
	 */
	public CarrierSL(int x_start,int y_start,int carry_weight)
	{
		this.carry_weight=carry_weight;
		this.id=CounterManager.getAndInc(ConfigurationParameters.ID_COUNTER); 
		
		BooleanManager.add(ConfigurationParameters.EXIT_AGENT_BASE_BOOL + String.valueOf(id),false);
		communicationManager=new CommunicationManager();
		contactManager= new ContactManager(this.id,communicationManager);
		movementManager= new MovementManager(this.id,x_start,y_start,1,1,contactManager);
		
		System.out.println(id + " Carrier - SubjectLayer initialized");
	}
	
	/**
	 * Put the agent to sleep for a number of milliseconds. 
	 * 
	 * @param sleepTime Number of ms that the agent will sleep 
	 * 
	 */	
	private void sleepAgent(int sleepTime) 
	{	
		try
		{
			Thread.sleep(sleepTime);
		} catch(InterruptedException ie) {}
	}
	

	
	/**
	 * Checks if an agent need assistance carrying an object, if true communicate with it and the reach its position.
	 * 
	 * @param  registration a reference to the role assigned to the agents, necessary to communicate with others 
	 */	
	private RoleRegistration helpManagerIfNeeded(RoleRegistration registration) throws RoleException
	{
		communicationManager.receiveEvent(registration);
		EventData data= communicationManager.requestEvent(Carrier.KE_reqHelp);
		
		if(data!=null) // If i received a help request, communicate and reach the sender 
		{
			DataOutputManager.printString(id +" Collaborative Carry Request Received. Sending data");			
			registration.doAction(Carrier.sendData(data.getSender(),new MessagePayload())); 
			
			while(true)
			{
				communicationManager.receiveEvent(registration);
				
				data= communicationManager.requestEvent(Carrier.KE_followMe);
				
				if(data!=null) 
				{
					DataOutputManager.printString(id +" Manager position received. Reaching");
					break; 			
				}
				
				if(communicationManager.requestEventExist(Carrier.KE_notNeeded)) 
				{
					DataOutputManager.printString(id +" I am not needed. Aborting collaboration");		
					return registration;
				}
			}
			
			registration.dismiss();	// Acquire the manager role
			registration=roleSystem.reqRegistration(Slave.ROLE_ID);
			
			movementManager.reachObject(data.getContent().getPosition()); 
			registration.doAction(Slave.agentReached(data.getSender(),new MessagePayload()));
			
			helpManagerWait(registration);	
			
			registration.dismiss();	// Acquire the manager role
			registration=roleSystem.reqRegistration(Carrier.ROLE_ID);
			
			return registration;
		}
		
		return null;
	}	
	
	/**
	 * Follow the movement instructions sent by the manager agent in order to carry the object to the base.
	 * 
	 * @param  registration a reference to the role assigned to the agents, necessary to communicate with others 
	 */	
	private void helpManagerWait(RoleRegistration registration) throws RoleException
	{
		DataOutputManager.dataSim.getAgentData(id).inctimesAsCollaborativeCarrier();
		
		if(ConfigurationParameters.COLLABORATIVE_TRANSPORT_INC_OBJ)
			DataOutputManager.dataSim.getAgentData(id).incobjectCarried();

		// Mi muovo con sincronizzazione verso base
		while(true)
		{
			communicationManager.receiveEvent(registration);
			EventData data= communicationManager.requestEvent(Slave.KE_moveOneSquare);
			
			if(data!=null)//Muoviti 
			{	
				movementManager.setPosition(data.getContent().getPosition());
				registration.doAction(Slave.moveAck(data.getSender()));			
			}
			
			if(communicationManager.requestEventExist(Slave.KE_carryFinished)) 
			{
				if(ConfigurationParameters.contactType==3)
					DataOutputManager.printString(id +" Wait Mode: Slave Trasport Ended");
				else
					DataOutputManager.printString(id +" Slave Trasport Ended");
				break;			
			}	
		}
	}
	
	/**
	 * The actions that need to be done on agent start.
	 *  
	 * @param rSystem A reference to the RoleSystem
	 * @return Reference to the role of the agent in the Role System.
	 */	
	private RoleRegistration start_operations(RoleSystem rSystem) throws RoleException
	{
		RoleRegistration registration;
		
		DataOutputManager.printString(id + " Agent Started");
		
		this.roleSystem=rSystem;
		
		try
		{
			registration=roleSystem.reqRegistration(Carrier.ROLE_ID);
		}
		catch(RoleException re)
		{
			System.err.println(re);
			return null;
		}
		
		DataOutputManager.printString(id + " Registration OK (Carrier)");		
		
		contactManager.setRoleSystem(roleSystem);
		
		CounterManager.inc(ConfigurationParameters.START_COUNTER);
		
		if(CounterManager.get(ConfigurationParameters.START_COUNTER)==ConfigurationParameters.NUMBER_OF_AGENTS_CARRY)
		{		
			DataOutputManager.printString("Agent count id" + CounterManager.get(ConfigurationParameters.ID_COUNTER));	
			DataOutputManager.dataSim.createAgentData(CounterManager.get(ConfigurationParameters.ID_COUNTER));	
			AgentMap.createAgentMap(CounterManager.get(ConfigurationParameters.ID_COUNTER));
		}	
		
		while(CounterManager.get(ConfigurationParameters.START_COUNTER)!=ConfigurationParameters.NUMBER_OF_AGENTS_CARRY)
		{
			sleepAgent(100);
		};
		
		movementManager.drawVisualization();
		
		return registration;	
	}
	
	/**
	 * The actions that need to be done before exit.
	 *  
	 * @param registration Reference to the role of the agent in the Role System, necessary to communicate with other agents.
	 * @return Reference to the role of the agent in the Role System.
	 */	
	private RoleRegistration exit_operations(RoleRegistration registration) throws RoleException
	{
		movementManager.reachObject(new PositionSq(1,1));
		
		DataOutputManager.printString(id + " EXIT");
		
		CounterManager.inc(ConfigurationParameters.EXIT_COUNTER);
		registration.dismiss();
		
		if(data_timer.isRunning() && BooleanManager.get(ConfigurationParameters.EXIT_BOOL)==true)
			data_timer.stop();	
		
		if(CounterManager.equals(ConfigurationParameters.EXIT_COUNTER, ConfigurationParameters.ID_COUNTER))
		{
			if(data_timer.isRunning())
				data_timer.stop();
			
			DataOutputManager.SimulationStop();
			end_timer.stop();
			
			DataOutputManager.closeAggregateDataFile();
		}	
		
		return registration;	
	}
	
	
	/**
	 * The actions that need to be done to reset the agent and the environment. 
	 * @throws RoleException 
	 * 
	 */	
	private synchronized void resetAgent() throws RoleException
	{	
		semaphore.acquireUninterruptibly();  
		
		CounterManager.inc(ConfigurationParameters.RESET_COUNTER);
		this.RESET_LOCAL=false;
		movementManager.reset(new PositionSq((int)(Math.random()*(EnvironmentManager.ENV_MAX_X-1)),
				(int)(Math.random()*(EnvironmentManager.ENV_MAX_Y-1))
				));  
		//communicationManager.clearAllEvents(registration);
		
		DataOutputManager.printString(id + " Agent Reset");
		
		if(CounterManager.equals(ConfigurationParameters.RESET_COUNTER, ConfigurationParameters.ID_COUNTER) && (BooleanManager.getAndSetIfFalse(ConfigurationParameters.ONLY_ONE_RESET_BOOL)==false))
		{
			EnvironmentManager.reset();
			EnvironmentManager.resetText();
			EnvironmentManager.updateVisualization();
			
			CounterManager.reset(ConfigurationParameters.TIMER_END_COUNTER);
			DataOutputManager.dataSim.clearAgentData();
			DataOutputManager.printString("Agent count id: " + CounterManager.get(ConfigurationParameters.ID_COUNTER));	
			DataOutputManager.dataSim.createAgentData(CounterManager.get(ConfigurationParameters.ID_COUNTER));
			
			BooleanManager.set(ConfigurationParameters.TIMER_END_BOOL,false);
			BooleanManager.set(ConfigurationParameters.RESET_BOOL,false);
			CounterManager.reset(ConfigurationParameters.RESET_COUNTER);
			
			DataOutputManager.printString("## Environment Reset");
		}	
		
		semaphore.release();	
		
		while(BooleanManager.get(ConfigurationParameters.STOP_BOOL)==true) 
		{
			sleepAgent(200);
		};
		
	}
	
	
	
	/**
	 * Checks if the agent need to stop and return to nest, or if it has been commanded to reset. 
	 * @throws RoleException 
	 * 
	 */	
	private void checkResetAndStop() throws RoleException
	{	
		if(BooleanManager.get(ConfigurationParameters.TIMER_END_BOOL)==true)
		{
			BooleanManager.set(ConfigurationParameters.STOP_BOOL,true);
			
			movementManager.reachObject(new PositionSq(1,1));
			
			CounterManager.inc(ConfigurationParameters.TIMER_END_COUNTER);
			
			if(data_timer.isRunning())
				data_timer.stop();	
			
			if(end_timer.isRunning())
				end_timer.stop();
			
			if(CounterManager.equals(ConfigurationParameters.TIMER_END_COUNTER, ConfigurationParameters.ID_COUNTER))
			{
				DataOutputManager.SimulationStop();
				DataOutputManager.aggregateDataToFile();
				DataOutputManager.endsimulation();
				
				EnvironmentManager.stopSignal();
			}	
			
		}	
		
		
		while(BooleanManager.get(ConfigurationParameters.STOP_BOOL)==true) 
		{
			sleepAgent(200);
			
			if(BooleanManager.get(ConfigurationParameters.RESET_BOOL)==true && this.RESET_LOCAL==true)
				resetAgent();
		};
		
		RESET_LOCAL=true;
		BooleanManager.set(ConfigurationParameters.ONLY_ONE_RESET_BOOL,false);
		CounterManager.reset(ConfigurationParameters.RESET_COUNTER);
		
		if(!end_timer.isRunning())
		{	
			end_timer.start();
			DataOutputManager.SimulationStart();
		}	
		
		if(!data_timer.isRunning())
			data_timer.start();
			
	}
	
	
	/**
	 * Actions to do if the object that the agent want to carry has weight>1 and the "Wait Mode" is active
	 *  
	 * @param registration Reference to the role of the agent in the Role System, necessary to communicate with other agents.
	 * @param goal Position of the object that the agent want to carry
	 * @param search_result Data about the object that the agent want to carry
	 * @return Reference to the role of the agent in the Role System
	 * @see RoleSystem
	 * @see PositionSq
	 */	
	private RoleRegistration ManagerWaitMode_Logic(RoleRegistration registration, PositionSq goal,ArrayList<SearchResult> search_result) throws RoleException
	{	
		int[] managerArray = this.roleSystem.searchForRole(Manager.ROLE_ID, 50);
		
		for(int i=0;i<managerArray.length;i++)					
			registration.doAction(Carrier.iAmHere(managerArray[i],new MessagePayload(-1,goal)));
		
		boolean needed=contactManager.CheckForSameManagerWait(registration); // Only one manager x one object 
		
		if(needed)
		{	
			registration.dismiss();	// Acquire the manager role
			registration=roleSystem.reqRegistration(Slave.ROLE_ID);
			
			helpManagerWait(registration);
			
			registration.dismiss();	// Acquire the manager role
			registration=roleSystem.reqRegistration(Carrier.ROLE_ID);
			
			return registration; 
		}
		
		if(EnvironmentManager.getReservedAndSetIfFalse(goal)) // combined method employing the synchronized construct to ensure that one agent access the same square at one time
		{	
			DataOutputManager.printString(id +" Wait Mode: Oggetto gestito da altro manager");
			movementManager.randomMovement();
			return registration;
		}
		
		if(EnvironmentManager.getObjectExist(goal)==false)
		{	
			DataOutputManager.printString(id + " Wait Mode: Object not found at destination");
			return registration;
		}	
		
		DataOutputManager.printString(id + " Carrier --> Manager");
		
		CounterManager.inc(ConfigurationParameters.MANAGER_COUNTER);
		
		registration.dismiss();	// Acquire the manager role
		registration=roleSystem.reqRegistration(Manager.ROLE_ID);
		DataOutputManager.dataSim.getAgentData(id).inctimesAsManager();
		
		ArrayList<CarrierData> risposte_carrier=contactManager.contactCarriers(registration, search_result.get(0).weight,new PositionSq(goal),movementManager.getPosition());
			
		if(contactManager.getWaitState()==true) 
			contactManager.waitForCarriers(registration, search_result.get(0).weight, goal,risposte_carrier); // Contact carriers and tell them to come to me
		
		DataOutputManager.printString(id +" Entire group arrived. Start collaborative transport");
		
		EnvironmentManager.pickObject(new PositionSq(goal));
		
		DataOutputManager.dataSim.getAgentData(id).incobjectCarried();
		
		end_timer.restart();
		
		movementManager.carryObjectGroup(this.roleSystem,registration,risposte_carrier);
		
		registration.dismiss();	// Acquire the carrier role
		registration=roleSystem.reqRegistration(Carrier.ROLE_ID);
		
		CounterManager.dec(ConfigurationParameters.MANAGER_COUNTER);
		
		return registration;
	}
	
	/**
	 * Actions to do if the object that the agent want to carry has weight>1 and the "Wait Mode" isn't active
	 *  
	 * @param registration Reference to the role of the agent in the Role System, necessary to communicate with other agents.
	 * @param goal Position of the object that the agent want to carry
	 * @param search_result Data about the object that the agent want to carry
	 * @return Reference to the role of the agent in the Role System
	 * @see RoleSystem
	 * @see PositionSq
	 */	
	private RoleRegistration Manager_Logic(RoleRegistration registration, PositionSq goal,ArrayList<SearchResult> search_result) throws RoleException
	{
		if(EnvironmentManager.getReservedAndSetIfFalse(goal)) 
		{	
			DataOutputManager.printString(id +" Oggetto gestito da altro manager");
			return registration;
		}
		
		boolean othermanager=contactManager.CheckForSameManager(registration,movementManager.getPosition()); // Only one manager x one object 
		
		if(othermanager)
		{	
			sleepAgent(500);
			
			movementManager.randomMovement();
			return registration;
		}
		
		DataOutputManager.printString(id + " Carrier --> Manager");
		
		CounterManager.inc(ConfigurationParameters.MANAGER_COUNTER);
		
		registration.dismiss();	// Acquire the manager role
		registration=roleSystem.reqRegistration(Manager.ROLE_ID);
		DataOutputManager.dataSim.getAgentData(id).inctimesAsManager();
		
		contactManager.ManagerCheckResponse(registration,movementManager.getPosition());
		
		ArrayList<CarrierData> risposte_carrier=contactManager.contactCarriers(registration, search_result.get(0).weight,new PositionSq(goal),movementManager.getPosition());
		
		contactManager.waitForCarriers(registration, search_result.get(0).weight, goal,risposte_carrier); // Contact carriers and tell them to come to me	
		
		DataOutputManager.printString(id +" Entire group arrived. Start collaborative transport");
		
		EnvironmentManager.pickObject(new PositionSq(goal));
		
		DataOutputManager.dataSim.getAgentData(id).incobjectCarried();
		
		end_timer.restart();
		
		movementManager.carryObjectGroup(this.roleSystem,registration,risposte_carrier);
		
		registration.dismiss();	// Acquire the carrier role
		registration=roleSystem.reqRegistration(Carrier.ROLE_ID);
		CounterManager.dec(ConfigurationParameters.MANAGER_COUNTER);
		
		return registration;
	}
	
	
	
	/**
	 * Actions to do if the object that the agent want to carry has weight=1
	 *  
	 * @param goal Position of the object that the agent want to carry
	 * @see RoleSystem
	 * @see PositionSq
	 */	
	private void Carrier_Logic(PositionSq goal) throws RoleException
	{
		Square test=EnvironmentManager.pickObject(new PositionSq(goal));
		
		if(test==null)
		{	
			DataOutputManager.printString(id + " Object not found at destination");
			DataOutputManager.dataSim.getAgentData(id).inclostObjects();
			return;
		}
		else
			DataOutputManager.dataSim.getAgentData(id).incobjectCarried();	
		
		movementManager.carryObjectSimple();
		
		return;
	}
	
	
	/**
	 * Method called by the RoleSystem on startup, contains the code managing the internal functioning of the agent
	 * 
	 * @param rSystem A reference to the RoleSystem
	 * @see RoleSystem
	 */	
	public void go(RoleSystem rSystem)
	{
		RoleRegistration registration;
		
		try
		{
			registration=start_operations(rSystem);
			
			if(registration==null)
				return;
			
			while(BooleanManager.get(ConfigurationParameters.EXIT_BOOL)==false && BooleanManager.get(ConfigurationParameters.EXIT_AGENT_BASE_BOOL + String.valueOf(id))==false)
			{			
				checkResetAndStop();  // Check if the GUI (or a timer) commanded a reset or a stop
				
				RoleRegistration new_registration=helpManagerIfNeeded(registration);
				
				if(new_registration!=null)
					registration=new_registration;
				
				ArrayList<SearchResult> search_result=movementManager.searchObject();	// Search for Objects	
				
				if(!search_result.isEmpty())
				{
					end_timer.restart();
					
					PositionSq goal=new PositionSq(search_result.get(0).found_position);
					
					DataOutputManager.printString(id +" Search concluded. Goal Set - X:" + goal.x + " Y:" + goal.y);
					
					movementManager.reachObject(new PositionSq(goal)); // Move towards the object	
					
					if(search_result.get(0).weight>this.carry_weight) //  If i need other agents to carry the object...
					{				
						if(ConfigurationParameters.contactType==3)
							registration = ManagerWaitMode_Logic(registration,new PositionSq(goal),search_result);
						else
							registration = Manager_Logic(registration,new PositionSq(goal),search_result);	
					}
					else
					{					
						Carrier_Logic(new PositionSq(goal));
					}	
					
				}
				else // if no object can be found in the search radius move at random
				{
					movementManager.randomMovement();
				}
			}	
			
			registration=exit_operations(registration);
		}	
		catch(RoleException re)
		{
			System.err.println(re);
		}
	}
}


